Co-simulation Interfaces ======================== The HELICS Interface -------------------- `Hierarchical Engine for Large-scale Infrastructure Co-Simulation (HELICS) `_ provides an open-source, general-purpose, modular, highly-scalable co-simulation framework that runs cross-platform (Linux, Windows, and Mac OS X). It is not a modeling tool by itself, but rather an integration tool that enables multiple existing simulation tools (known as "federates") to exchange data during runtime and stay synchronized in time such that together they act as one large simulation, or "federation". The HELICS interface for PyDSS is built to reduce the complexity of setting up large-scale co-simulation scenarios. The user defines publications and subscriptions to exchange data with external federates. A minimal HELICS example is availble in the ``examples`` folder (top directory of the repository). Enabling the HELICS interface requires user to define additional parammeters in the scenario TOML file. Interface Overview ^^^^^^^^^^^^^^^^^^ The HELICS interface can be enabled and configured using the ``simulation.toml`` file. The following attributes can be configured: .. autopydantic_model:: pydss.simulation_input_models.HelicsModel - ``co_simulation_mode``: Set to ``true`` to enable the HELICS interface (default: ``false``). - ``federate_name``: Required to identify a federate in a co-simulation with many federates. - Additional settings for convergence, timing, and iteration can be configured here. For more information on these values, refer to the `HELICS documentation `_. Once the HELICS co-simulation interface has been enabled, the next step is to set up ``publications`` and ``subscriptions`` for data exchange with external federates. PyDSS enables zero-code setup of these modules. Each scenario can have its own publication and subscription definitions, managed by files in the ``ExportLists`` directory. Publication tags (names) follow this convention: .. code-block:: ... examples, federate1.Circuit.70008.TotalPower federate1.Load.load_1.VoltageMagAng where ``federate name`` is defined in the project's ``simulation.toml`` file. Setting up Publications ^^^^^^^^^^^^^^^^^^^^^^^ Publications (data sent to external federates) can be configured using ``Exports.toml``. This file is also used to define export variables for a simulation scenario. By setting the ``publish`` attribute to ``true``, PyDSS automatically sets up a HELICS publication. The file supports multiple filtering options including regex to publish only what is needed. examples: The following example sets up publications for all PV system powers in the model. Setting ``publish`` to ``false`` will still write the data to the HDF5 store, but will not publish it on the HELICS interface. .. code-block:: toml [[PVSystems]] property = "Powers" sample_interval = 1 publish = true store_values_type = "all" There are two options to filter and publish a subset of elements. You can use the ``name_regexes`` attribute to filter elements matching regex expressions, or use the ``names`` attribute to explicitly list elements. Filtering using regex expressions: .. code-block:: toml [[PVSystems]] property = "Powers" name_regexes = [".*pvgnem.*"] sample_interval = 1 publish = true store_values_type = "all" Filtering using explicit element names: .. code-block:: toml [[PVSystems]] property = "Powers" sample_interval = 1 names = ["PVSystems.pv1", "PVSystems.pv2"] publish = true store_values_type = "all" Setting up Subscriptions ^^^^^^^^^^^^^^^^^^^^^^^^ Subscriptions (data received from external federates) are configured using ``Subscriptions.toml`` in the ``ExportLists`` directory for a given scenario. Valid subscriptions should conform to the following model: .. autopydantic_model:: pydss.helics_interface.Subscription When setting up subscriptions, note that the subscription tag is generated by the external federate and must be known before configuration. In the example below, values received from subscription tag ``test.load1.power`` are used to update the ``kw`` property of load ``Load.mpx000635970``. The ``multiplier`` property can scale values before they update the model. example .. code-block:: toml [[subscriptions]] model = "Load.mpx000635970" property = "kw" id = "test.load1.power" unit = "kW" subscribe = true data_type = "double" multiplier = 1 A complete example is available in ``examples/external_interfaces/``. The Socket Interface -------------------- The socket interface is implemented as a PyDSS controller (:py:class:`pydss.pyControllers.Controllers.SocketController.SocketController`). It is well suited for situations where an existing external controller needs to be integrated into the simulation environment — for example, integrating a controller for thermostatically controlled loads implemented in Modelica or Python. This allows integration without modifying the external controller. The socket interface is also useful for hardware-in-the-loop simulations, integrating the simulation engine with actual hardware. Interfaces for Modbus-TCP and DNP3 communications have been developed and tested with PyDSS. A minimal socket example is provided in ``examples/external_interfaces/pydss_project``. The ``socket`` scenario defines the socket controller in its ``pyControllerList`` folder. The controller configuration specifies the element to control, the socket connection details, and the data to exchange: .. code-block:: toml ["Load.mpx000635970"] IP = "127.0.0.1" Port = 5001 Encoding = false Buffer = 1024 Index = "Even,Even" Inputs = "VoltagesMagAng,Powers" Outputs = "kW" Finally, the minimal example below shows how to retrive data from the sockets and return new values for parameters defined in the definations file. .. code-block:: python # first of all import the socket library import socket import struct # next create a socket object sockets = [] for i in range(2): s = socket.socket() s.bind(('127.0.0.1', 5001 + i)) s.listen(5) sockets.append(s) while True: # Establish connection with client. conns = [] for s in sockets: c, addr = s.accept() conns.append(c) while True: for c in conns: #Reading data from all ports Data = c.recv(1024) if Data: #Creating a list of doubles from the recieved byte stream numDoubles = int(len(Data) / 8) tag = str(numDoubles) + 'd' Data = list(struct.unpack(tag, Data)) for c , v in zip(conns, [5, 3]): #Writing data to all ports values = [v] c.sendall(struct.pack('%sd' % len(values), *values))